﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;

namespace Aeon.Emulator.Keyboard
{
    /// <summary>
    /// Emulates a physical keyboard device and handles related interrupts.
    /// </summary>
    internal sealed class KeyboardDevice : IInterruptHandler, IInputPort, IOutputPort
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the KeyboardDevice class.
        /// </summary>
        public KeyboardDevice()
        {
            foreach(Keys k in Enum.GetValues(typeof(Keys)))
                pressedKeys[k] = false;
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets a value indicating whether the type-ahead buffer has at least one character in it.
        /// </summary>
        public bool HasTypeAheadDataAvailable
        {
            get
            {
                return !typeBuffer.IsEmpty;
            }
        }
        /// <summary>
        /// Gets a value indicating whether the hardware queue is empty.
        /// </summary>
        public bool IsHardwareQueueEmpty
        {
            get
            {
                return hardwareQueue.IsEmpty;
            }
        }
        #endregion

        #region Private Properties
        /// <summary>
        /// Gets or sets the current byte in the hardware queue.
        /// </summary>
        private byte? CurrentHardwareByte { get; set; }
        #endregion

        #region Public Methods
        /// <summary>
        /// Simulates a key press on the emulated keyboard.
        /// </summary>
        /// <param name="key">Key pressed on the keyboard.</param>
        public void PressKey(Keys key)
        {
            if(!pressedKeys[key])
            {
                pressedKeys[key] = true;
                HardwareEnqueue(key, true);
            }
        }
        /// <summary>
        /// Simulates a key release on the emulated keyboard.
        /// </summary>
        /// <param name="key">Key released on the keyboard.</param>
        public void ReleaseKey(Keys key)
        {
            if(pressedKeys[key])
            {
                pressedKeys[key] = false;
                HardwareEnqueue(key, false);
            }
        }
        /// <summary>
        /// Reads a single character from the type-ahead buffer.
        /// </summary>
        /// <returns>Character read from the type-ahead buffer if one is available; otherwize zero.</returns>
        public ushort DequeueTypeAhead()
        {
            return typeBuffer.Dequeue((ushort)0);
        }
        /// <summary>
        /// Returns the character read from the type-ahead buffer if one is available.
        /// </summary>
        /// <returns>Character read from the type-ahead buffer if one is available; otherwise null.</returns>
        public ushort? TryDequeueTypeAhead()
        {
            ushort result;
            if(typeBuffer.TryDequeue(out result))
                return result;
            else
                return null;
        }
        /// <summary>
        /// Dequeues the next hardware input byte.
        /// Must be called when IRQ 1 is handled.
        /// </summary>
        public void BeginHardwareInterrupt()
        {
            byte value;
            if(hardwareQueue.TryDequeue(out value))
                this.CurrentHardwareByte = value;
            else
                this.CurrentHardwareByte = null;
        }
        #endregion

        #region Private Methods
        private void ProcessScanCode(uint scanCode)
        {
            bool isModifier = true;

            switch(scanCode)
            {
            case (uint)Keys.LeftShift:
                leftShiftDown = true;
                modifiers |= KeyModifiers.Shift;
                break;

            case (uint)Keys.RightShift:
                rightShiftDown = true;
                modifiers |= KeyModifiers.Shift;
                break;

            case (uint)Keys.Ctrl:
            case (uint)Keys.RightCtrl:
                modifiers |= KeyModifiers.Ctrl;
                break;

            case (uint)Keys.Alt:
            case (uint)Keys.RightAlt:
                modifiers |= KeyModifiers.Alt;
                break;

            case (uint)Keys.CapsLock:
                modifiers ^= KeyModifiers.CapsLock;
                break;

            case (uint)Keys.NumLock:
                modifiers ^= KeyModifiers.NumLock;
                break;

            case (uint)Keys.ScrollLock:
                modifiers ^= KeyModifiers.ScrollLock;
                break;

            case (uint)Keys.Insert:
                modifiers |= KeyModifiers.Insert;
                break;

            case LeftShiftUp:
                leftShiftDown = false;
                if(!leftShiftDown && !rightShiftDown)
                    modifiers &= ~KeyModifiers.Shift;
                break;

            case RightShiftUp:
                rightShiftDown = false;
                if(!leftShiftDown && !rightShiftDown)
                    modifiers &= ~KeyModifiers.Shift;
                break;

            case CtrlUp:
                modifiers &= ~KeyModifiers.Ctrl;
                break;

            case AltUp:
                modifiers &= ~KeyModifiers.Alt;
                break;

            case InsertUp:
                modifiers &= ~KeyModifiers.Insert;
                break;

            default:
                isModifier = false;
                break;
            }

            if(!isModifier)
            {
                ushort keyCode = ScanCodeConverter.ConvertToKeyboardCode((byte)(scanCode & 0xFF), modifiers);
                if(keyCode != 0)
                {
                    vm.Processor.CX = (short)keyCode;
                    vm.Processor.AH = Functions.StoreKeyCodeInBuffer;
                    vm.RaiseInterrupt(0x16);
                }
            }
        }
        /// <summary>
        /// Handles the 9h hardware interrupt.
        /// </summary>
        private void HandleInt9h()
        {
            // Send end of interrupt signal.
            vm.WritePortByte(0x20, 0x20);

            byte? nullableValue = this.CurrentHardwareByte;
            if(nullableValue == null)
                return;

            byte value = (byte)nullableValue;
            if(expectedInputByteCount == 0)
            {
                if(value == 0xE0)
                    expectedInputByteCount = 1;
                else if(value == 0xE1)
                    expectedInputByteCount = 5;
                else
                {
                    ProcessScanCode(value);
                    return;
                }
            }
            else
                expectedInputByteCount--;

            internalBuffer.Add(value);

            if(expectedInputByteCount == 0)
            {
                if(internalBuffer[0] != 0xE1)
                {
                    uint scanCode = (uint)(internalBuffer[1] | (internalBuffer[0] << 8));
                    ProcessScanCode(scanCode);
                }

                internalBuffer.Clear();
            }
        }
        /// <summary>
        /// Handles the 16h BIOS interrupt.
        /// </summary>
        private void HandleInt16h()
        {
            switch(vm.Processor.AH)
            {
            case Functions.ReadCharacter:
            case Functions.ReadExtendedCharacter:
                ReadCharacter();
                break;

            case Functions.CheckForCharacter:
            case Functions.CheckForExtendedCharacter:
                CheckForCharacter();
                SaveFlags(EFlags.Zero);
                break;

            case Functions.GetShiftFlags:
                GetShiftFlags();
                break;

            case Functions.StoreKeyCodeInBuffer:
                typeBuffer.Enqueue((ushort)vm.Processor.CX);
                break;

            case Functions.GetExtendedShiftFlags:
                GetShiftFlags();
                break;

            default:
                System.Diagnostics.Debug.Write("Unknown int16 command.");
                break;
            }
        }
        /// <summary>
        /// Reads a single character from the type-ahead buffer.
        /// Moves Processor.EIP back to reissue this call until something is returned.
        /// </summary>
        private void ReadCharacter()
        {
            if(!typeBuffer.IsEmpty)
                vm.Processor.AX = (short)DequeueTypeAhead();
            else
            {
                // Run this interrupt handler again if there is nothing to read yet.
                vm.Processor.EIP -= 3;
                vm.Processor.Flags |= EFlags.InterruptEnable;
            }
        }
        /// <summary>
        /// Reads a single character from the type-ahead buffer if one is available.
        /// </summary>
        private void CheckForCharacter()
        {
            if(!typeBuffer.IsEmpty)
            {
                vm.Processor.AX = (short)typeBuffer.Peek((ushort)0);
                vm.Processor.ZeroFlag = false;
            }
            else
            {
                vm.Processor.AX = 0;
                vm.Processor.ZeroFlag = true;
            }
        }
        /// <summary>
        /// Places the scancode for a key press/release in the emulated keyboard buffer.
        /// </summary>
        /// <param name="key">Key pressed or released.</param>
        /// <param name="isPressed">Value indicating whether key is pressed.</param>
        private void HardwareEnqueue(Keys key, bool isPressed)
        {
            if(key >= Keys.Esc && key <= Keys.KeypadPeriod)
            {
                if(isPressed)
                    hardwareQueue.Enqueue((byte)key);
                else
                    hardwareQueue.Enqueue((byte)((byte)key + 0x80));
            }
            else if(key == Keys.Pause)
            {
                if(isPressed)
                {
                    hardwareQueue.Enqueue(0xE1);
                    hardwareQueue.Enqueue(0x1D);
                    hardwareQueue.Enqueue(0x45);
                    hardwareQueue.Enqueue(0xE1);
                    hardwareQueue.Enqueue(0x9D);
                    hardwareQueue.Enqueue(0xC5);
                }
            }
            else if(((int)key & 0xE0) == 0xE0)
            {
                if(isPressed)
                {
                    hardwareQueue.Enqueue(0xE0);
                    hardwareQueue.Enqueue((byte)(((int)key >> 8) & 0xFF));
                }
                else
                    hardwareQueue.Enqueue((byte)((((int)key >> 8) & 0xFF) + 0x80));
            }
        }
        private void SaveFlags(EFlags modified)
        {
            var oldFlags = (EFlags)vm.PhysicalMemory.GetUInt16(vm.Processor.SS, (ushort)(vm.Processor.SP + 4));
            oldFlags &= ~modified;
            vm.PhysicalMemory.SetUInt16(vm.Processor.SS, (ushort)(vm.Processor.SP + 4), (ushort)(oldFlags | (vm.Processor.Flags & modified)));
        }
        private void GetShiftFlags()
        {
            byte value = 0;

            if((modifiers & KeyModifiers.Shift) == KeyModifiers.Shift)
                value |= 2;
            if((modifiers & KeyModifiers.Ctrl) == KeyModifiers.Ctrl)
                value |= 4;
            if((modifiers & KeyModifiers.Alt) == KeyModifiers.Alt)
                value |= 8;

            vm.Processor.AL = value;
        }
        #endregion

        #region IInterruptHandler Members
        IEnumerable<InterruptHandlerInfo> IInterruptHandler.HandledInterrupts
        {
            get { return new InterruptHandlerInfo[] { new InterruptHandlerInfo(0x09, Registers.AX | Registers.CX), 0x16 }; }
        }
        void IInterruptHandler.HandleInterrupt(int interrupt)
        {
            if(interrupt == 0x09)
                HandleInt9h();
            else if(interrupt == 0x16)
                HandleInt16h();
        }
        #endregion

        #region IInputPort Members
        IEnumerable<int> IInputPort.InputPorts
        {
            get
            {
                return new int[] { 0x60, 0x64 };
            }
        }
        byte IInputPort.ReadByte(int port)
        {
            switch(port)
            {
            case 0x60:
                return this.CurrentHardwareByte ?? 0;

            case 0x64:
                return 0;

            default:
                throw new ArgumentException();
            }
        }
        ushort IInputPort.ReadWord(int port)
        {
            throw new NotSupportedException();
        }
        #endregion

        #region IOutputPort Members
        IEnumerable<int> IOutputPort.OutputPorts
        {
            get
            {
                return new int[] { 0x60, 0x64 };
            }
        }
        void IOutputPort.WriteByte(int port, byte value)
        {
            if(port == 0x60)
                vm.PhysicalMemory.EnableA20 = !vm.PhysicalMemory.EnableA20;

            System.Diagnostics.Debug.WriteLine(string.Format("Keyboard port {0:X}h -> {1:X2}h", port, value));
        }
        void IOutputPort.WriteWord(int port, ushort value)
        {
            throw new NotSupportedException();
        }
        #endregion

        #region IVirtualDevice Members
        void IVirtualDevice.Pause()
        {
        }
        void IVirtualDevice.Resume()
        {
        }
        /// <summary>
        /// Invoked when the virtual device has been added to a VirtualMachine.
        /// </summary>
        /// <param name="vm">VirtualMachine which owns the device.</param>
        void IVirtualDevice.DeviceRegistered(VirtualMachine vm)
        {
            this.vm = vm;
        }
        /// <summary>
        /// Invoked when the device should save its current state to a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="destination"><see cref="System.IO.Stream"/> to which the state is saved.</param>
        void IVirtualDevice.SaveCurrentState(Stream destination)
        {
            var writer = new BinaryWriter(destination);
            writer.Write(this.hardwareQueue.Count);
            foreach(var b in this.hardwareQueue)
                writer.Write(b);

            writer.Write(this.internalBuffer.Count);
            writer.Write(this.internalBuffer.ToArray());

            writer.Write(this.typeBuffer.Count);
            foreach(var s in this.typeBuffer)
                writer.Write(s);

            writer.Write((int)this.modifiers);
            writer.Write(this.leftShiftDown);
            writer.Write(this.rightShiftDown);

            writer.Write(this.pressedKeys.Count);
            foreach(var pair in this.pressedKeys)
            {
                writer.Write((int)pair.Key);
                writer.Write(pair.Value);
            }

            writer.Write(this.expectedInputByteCount);

            writer.Write(this.CurrentHardwareByte.HasValue);
            if(this.CurrentHardwareByte.HasValue)
                writer.Write(this.CurrentHardwareByte.Value);
        }
        /// <summary>
        /// Invoked when the device should restore its state from a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="source"><see cref="System.IO.Stream"/> from which the state is restored.</param>
        void IVirtualDevice.RestoreState(Stream source)
        {
            var reader = new BinaryReader(source);
            this.hardwareQueue.Clear();
            int count = reader.ReadInt32();
            for(int i = 0; i < count; i++)
                this.hardwareQueue.Enqueue(reader.ReadByte());

            this.internalBuffer.Clear();
            count = reader.ReadInt32();
            this.internalBuffer.AddRange(reader.ReadBytes(count));

            this.typeBuffer.Clear();
            count = reader.ReadInt32();
            for(int i = 0; i < count; i++)
                this.typeBuffer.Enqueue(reader.ReadUInt16());

            this.modifiers = (KeyModifiers)reader.ReadInt32();
            this.leftShiftDown = reader.ReadBoolean();
            this.rightShiftDown = reader.ReadBoolean();

            this.pressedKeys.Clear();
            count = reader.ReadInt32();
            for(int i = 0; i < count; i++)
            {
                var key = (Keys)reader.ReadInt32();
                var value = reader.ReadBoolean();
                this.pressedKeys[key] = value;
            }

            this.expectedInputByteCount = reader.ReadInt32();

            if(reader.ReadBoolean())
                this.CurrentHardwareByte = reader.ReadByte();
            else
                this.CurrentHardwareByte = null;
        }
        #endregion

        #region IDisposable Members
        void IDisposable.Dispose()
        {
        }
        #endregion

        #region Private Constants
        private const ushort LeftShiftUp = 0xAA;
        private const ushort RightShiftUp = 0xB6;
        private const ushort CtrlUp = 0x9D;
        private const ushort AltUp = 0xB8;
        private const ushort InsertUp = 0xD2;
        #endregion

        #region Private Fields
        private VirtualMachine vm;
        private readonly ConcurrentQueue<byte> hardwareQueue = new ConcurrentQueue<byte>();
        private readonly List<byte> internalBuffer = new List<byte>();
        private readonly ConcurrentQueue<ushort> typeBuffer = new ConcurrentQueue<ushort>();
        private KeyModifiers modifiers;
        private bool leftShiftDown;
        private bool rightShiftDown;
        private readonly SortedList<Keys, bool> pressedKeys = new SortedList<Keys, bool>();
        private int expectedInputByteCount;
        #endregion
    }
}
